package net.protocol.diameter.avp;

import net.protocol.common.util.DateUtils;
import net.protocol.common.util.IoUtils;
import net.protocol.diameter.dictionary.AVPFormat;
import net.protocol.diameter.dictionary.Formatter;
import net.protocol.diameter.dictionary.RLevel;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author jinhongw@gmail.com
 */
public class GenerateAVP {
    private final static String BASE_CODE_DIR = "standards/src/";
    private final static String BASE_CLASSES_DIR = "standards/classes/";

    private final static String DIR = "/"
            + GenerateAVP.class.getPackage().getName().replaceAll("\\.", "/") + "/";

    private final static String V_TPL = DIR + "AVP_V.TPL";
    private final static String NO_V_TPL = DIR + "AVP_NO_V.TPL";
    
    private final static String ENUM_V_TPL = DIR + "AVP_Enum_V.TPL";
    private final static String ENUM_NO_V_TPL = DIR + "AVP_Enum_NO_V.TPL";

    private final static String G_V_TPL = DIR + "G_AVP_V.TPL";
    private final static String G_NO_V_TPL = DIR + "G_AVP_NO_V.TPL";

    /**
     * Create specified AVP according to the given AVPFormat
     *
     * @param format diameter AVP format
     * @return the create java file representation of this AVP
     * @throws IOException if an I/O error occurs writing to or creating the file
     */
    public static File generateFile(AVPFormat format) throws IOException {
        return generateFile(format, null, null, false);
    }
    
    /**
     * Create specified Enumerated type AVP according to the given parameters
     * 
     * <pre>
     * Collection&lt;String[]&gt; enums form as follows:
     *     Collection&lt;new String[] {value, name, description}&gt;
     * For example:
     *     For IP-CAN-Type Enumerated AVP  	
     *         enums.add(new String[] {"0", "3GPP-GPRS", "...with a 3GPP GPRS access..."})
     *         enums.add(new String[] {"5", "3GPP-EPS", "...with a 3GPP EPS access..."})
     *         ...
     * </pre>
     * 
     * @param format diameter AVP format
     * @param enums the values of this Enumerated type AVP
     * @return the create java file representation of this AVP
     * @throws IOException
     */
	public static File generateEnumeratedFile(AVPFormat format,
			Collection<String[]> enums) throws IOException {
		if (format.getDataFormat() != DataFormat.Enumerated)
			throw new IllegalArgumentException(
					"DataFormat must be Enumerated type.");
		return generateFile(format, null, enums, true);
	}
	
	private static File generateFile(AVPFormat format, String pkg,
			Collection<String[]> enums, boolean isEnum) throws IOException {
		Iterable<String> src = generate(format, pkg, enums, isEnum);
		File file = getJava(BASE_CLASSES_DIR, format, pkg);
		IoUtils.write(file, src);
		IoUtils.write(getJava(BASE_CODE_DIR, format, pkg), src);
		return file;
	}

	private static Iterable<String> generate(AVPFormat format, String pkg,
			Collection<String[]> enums, boolean isEnum) throws IOException {
		if (format == null) return null;
		if (format.getDataFormat() != DataFormat.Enumerated) enums = null;
		
		String path = getTplFile(format, isEnum);
		InputStream in = GenerateAVP.class.getResourceAsStream(path);
		Collection<String> lines = IoUtils.readAllLines(in);
		return filter(lines, format, pkg, enums, isEnum);
	}

    private static File getJava(String baseDir,
            AVPFormat format, String pkg) throws IOException {
        // generate\src\net\protocol\diameter\base\avp\SessionId.java
        String path = getPath(baseDir, format, pkg) + ".java";
        return new File(path);
    }

    private static String getPath(String baseDir,
            AVPFormat format, String pkg) throws IOException {
        String dir = baseDir + getPackage(pkg, format).replaceAll("\\.", "/") + "/";
        File df = new File(dir);
        if (!df.exists()) {
            if (!df.mkdirs()) throw new IOException(dir + " can't be created.");
        }
        return dir + getClassName(format);
    }

	private static String getTplFile(AVPFormat avpFormat, boolean isEnum) {
		DataFormat format = avpFormat.getDataFormat();
		RLevel vendor = avpFormat.getFlagRule().getVendor();
		if (format == DataFormat.Grouped) {
			switch (vendor) {
			case MUSTNOT:
			case SHOULDNOT:
				return G_NO_V_TPL;
			default:
				return G_V_TPL;
			}
		}
		if (format == DataFormat.Enumerated) {
			switch (vendor) {
			case MUSTNOT:
			case SHOULDNOT:
				return isEnum ? ENUM_NO_V_TPL : NO_V_TPL;
			default:
				return isEnum ? ENUM_V_TPL : V_TPL;
			}
		} else {
			switch (vendor) {
			case MUSTNOT:
			case SHOULDNOT:
				return NO_V_TPL;
			default:
				return V_TPL;
			}
		}
	}

	private static Iterable<String> filter(Collection<String> list,
			AVPFormat format, String pkg,
			Collection<String[]> enums, boolean isEnum) {
        List<String> result = new ArrayList<>();
        String title = null;
        for (String e : list) {
            String code = e;

            code = code.replaceAll("\\$Package", getPackage(pkg, format));
            code = code.replaceAll("\\$import", getImport(format));
            code = code.replaceAll("\\$GenDate", getGenDate());

            if (e.contains(Formatter.NAME) &&
                    e.contains(Formatter.CODE) &&
                    e.contains(Formatter.DATA_TYPE)) {
                title = e;
            }
            if (title != null && e.contains("$Define")) {
                code = code.replaceAll("\\$Define", Formatter.format(title, format));
            }
            code = code.replaceAll("\\$AVP", getClassName(format));
            code = code.replaceAll("\\$Format", format.getDataFormat().name());
            code = code.replaceAll("\\$Code", Integer.toString(format.getCode()));
            code = code.replaceAll("\\$VendorId", getVendorId(format));
            code = code.replaceAll("\\$Name", format.getName());
            code = code.replaceAll("\\$standard", format.getStandard());
            code = code.replaceAll("\\$DataType", getDataType(format, isEnum));
            code = code.replaceAll("\\$DataPara", getDataPara(format));
            code = code.replaceAll("\\$DataValue", getDataValue(format, isEnum));
            code = code.replaceAll("\\$Description", 
            		format.getDescription() == null ? "" : format.getDescription());
            if (isEnum) code = code.replaceAll("\\$Enums", getEnums(enums));
            result.add(code);
        }
        return result;
    }

    private static String getPackage(String pkg, AVPFormat format) {
        String std = format.getStandard().toLowerCase() + ".avp";
        String base = AVP.class.getPackage().getName();
        int index = base.lastIndexOf(".");
        base = base.substring(0, index);
        return (pkg == null || pkg.trim().equals(""))
                ? (base + "." + std) : (pkg + "." + std);
    }

    private static String getImport(AVPFormat format) {
        StringBuilder buf = new StringBuilder();
        switch (format.getDataFormat()) {
            case Address:
                buf.append("import java.net.InetAddress;");
                break;
            case Time:
                buf.append("import java.util.Date;");
                break;
            case Grouped:
                String pkg = AVPFormat.class.getPackage().getName();
                buf.append("\n").append("import ").append(pkg).append(".*;");
                break;
            default:
                break;
        }

        String pkg = AVP.class.getPackage().getName();
        buf.append("\n").append("import ").append(pkg).append(".*;");
        return buf.toString();
    }

    private static String getGenDate() {
        long mills = System.currentTimeMillis();
        return DateUtils.format(mills);
    }

    private static String getClassName(AVPFormat format) {
        return format.getName().replaceAll("-", "");
    }
    
    private static String getVendorId(AVPFormat format) {
    	int id = format.getVendorId();
    	if (id == AbstractAVP.DEFAULT_VENDOR_ID) 
    		return "AbstractAVP.DEFAULT_VENDOR_ID";
    	return Integer.toString(id);
    }

    private static String getDataType(AVPFormat format, boolean isEnum) {
        switch (format.getDataFormat()) {
            case OctetString:
            case UTF8String:
            case DiameterIdentity:
            case QoSFilterRule:
            case IPFilterRule:
                return "String";
            case Unsigned32:
            case Integer64:
            case Unsigned64:
                return "long";
            case Integer32:
            case Enumerated:
                return isEnum ? "E" : "int";
            case Float32:
                return "float";
            case Float64:
                return "double";
            case Time:
                return "Date";
            case Address:
                return "InetAddress";
            case DiameterURI:
                return "DiameterURI.V";
            case Grouped:
                return "Collection<AVP<?>>";
            default:
                return null;
        }
    }

    private static String getDataPara(AVPFormat format) {
        switch (format.getDataFormat()) {
            case Address:
                return "address";
            case DiameterURI:
                return "uri";
            case IPFilterRule:
            case QoSFilterRule:
                return "rule";
            case Grouped:
                return "avps";
            default:
                return "data";
        }
    }

    private static String getDataValue(AVPFormat format, boolean isEnum) {
        switch (format.getDataFormat()) {
            case UTF8String:
                return "data.getBytes(UTF8_CHARSET)";
            case OctetString:
                return "data.getBytes()";
            case Enumerated:
                return isEnum ? "data.getValue()" : "data";
            case Address:
                return "address";
            case DiameterURI:
                return "uri";
            case IPFilterRule:
            case QoSFilterRule:
                return "rule";
            case Grouped:
                return "avps";
            default:
                return "data";
        }
    }
    
	private static String getEnums(Collection<String[]> enums) {
		if (enums == null || enums.isEmpty())
			throw new IllegalArgumentException("Enums must is not empty.");
		String prefix = "        ";
		StringBuilder buf = new StringBuilder();
		int i = 0;
		for (String[] a : enums) {
			if (i != 0) buf.append("\n").append(prefix);
			boolean hasDesc = false;
			if (a.length > 2 && (a[2] != null && !a[2].trim().equals(""))) {
				buf.append("\n").append(prefix);
				buf.append("/**\n");
				buf.append(prefix).append(" * ");
				buf.append(a[2].replaceAll("\n", "\n" + prefix + " * "));
				buf.append("\n").append(prefix).append(" */\n");
				hasDesc = true;
			} 
			if (hasDesc) buf.append(prefix);
			buf.append(a[1]).append("(").append(a[0]).append("),");
			i++;
		}
		buf.deleteCharAt(buf.length() - 1);
		buf.append(";");
		return buf.toString();
	}
}
